/*
 * Copyright 2020-2021 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cloud.xuxiaowei.passport.config;

import cloud.xuxiaowei.core.properties.SecurityProperties;
import cloud.xuxiaowei.core.utils.SecurityUtils;
import cloud.xuxiaowei.oauth2.config.CsrfTokenRepositoryConfig;
import cloud.xuxiaowei.oauth2.handler.ResourceAccessDeniedHandler;
import cloud.xuxiaowei.oauth2.matcher.AuthorizePermitAllRequestMatcher;
import cloud.xuxiaowei.oauth2.matcher.CsrfIgnoreRequestMatcher;
import cloud.xuxiaowei.oauth2.point.ResourceServerAuthenticationEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CsrfTokenRepository;

import javax.sql.DataSource;
import java.security.interfaces.RSAPublicKey;
import java.util.List;

/**
 * @author Joe Grandja
 * @author xuxiaowei
 * @since 0.0.1
 */
@Configuration
public class ResourceServerConfig {

	private SecurityProperties securityProperties;

	/**
	 * @see CsrfTokenRepositoryConfig#csrfTokenRepository()
	 */
	private CsrfTokenRepository csrfTokenRepository;

	private AuthorizePermitAllRequestMatcher[] authorizePermitAllRequestMatchers = new AuthorizePermitAllRequestMatcher[0];

	private CsrfIgnoreRequestMatcher[] csrfIgnoreRequestMatchers = new CsrfIgnoreRequestMatcher[0];

	@Autowired
	public void setSecurityProperties(SecurityProperties securityProperties) {
		this.securityProperties = securityProperties;
	}

	@Autowired
	public void setCsrfTokenRepository(CsrfTokenRepository csrfTokenRepository) {
		this.csrfTokenRepository = csrfTokenRepository;
	}

	@Autowired(required = false)
	public void setAuthorizePermitAllRequestMatchers(
			AuthorizePermitAllRequestMatcher[] authorizePermitAllRequestMatchers) {
		this.authorizePermitAllRequestMatchers = authorizePermitAllRequestMatchers;
	}

	@Autowired(required = false)
	public void setCsrfIgnoreRequestMatchers(CsrfIgnoreRequestMatcher[] csrfIgnoreRequestMatchers) {
		this.csrfIgnoreRequestMatchers = csrfIgnoreRequestMatchers;
	}

	@Bean
	public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {

		String loginProcessingUrl = securityProperties.getLoginProcessingUrl();
		String loginPageUrl = securityProperties.getLoginPageUrl();
		String failureForwardUrl = securityProperties.getFailureForwardUrl();
		String successForwardUrl = securityProperties.getSuccessForwardUrl();
		List<SecurityProperties.RequestMatcher> requestMatchers = securityProperties.getRequestMatchers();

		http.authorizeRequests(customizer -> {

			customizer.mvcMatchers(failureForwardUrl, successForwardUrl).permitAll();

			customizer.requestMatchers(authorizePermitAllRequestMatchers).permitAll();

			SecurityUtils.authorizeRequests(requestMatchers, customizer);

			// 其他地址：需要授权访问，此配置要放在最后一行
			customizer.anyRequest().authenticated();

		});

		http.formLogin(customizer -> {
			customizer
				// 登录请求 URL
				.loginProcessingUrl(loginProcessingUrl)
				// 登录页面 URL
				// 根据源码分析，如果需要设置了 loginPage，并且在 loginPage 之前未设置 loginProcessingUrl，
				// 则 loginProcessingUrl 会与 loginPage 保持一致
				.loginPage(loginPageUrl)
				// 登录失败重定向 URL
				.failureForwardUrl(failureForwardUrl)
				// 登录成功重定向 URL
				.successForwardUrl(successForwardUrl);
		});

		http.oauth2ResourceServer(customizer -> {
			customizer.jwt(jwtCustomizer -> {
				RSAPublicKey rsaPublicKey = securityProperties.rsaPublicKey();
				NimbusJwtDecoder.PublicKeyJwtDecoderBuilder publicKeyJwtDecoderBuilder = NimbusJwtDecoder
					.withPublicKey(rsaPublicKey);
				NimbusJwtDecoder nimbusJwtDecoder = publicKeyJwtDecoderBuilder.build();
				jwtCustomizer.decoder(nimbusJwtDecoder);
			});
		});

		http.oauth2ResourceServer(customizer -> customizer
			// 资源服务 身份验证入口点
			.authenticationEntryPoint(new ResourceServerAuthenticationEntryPoint())
			// 资源访问拒绝 处理程序
			.accessDeniedHandler(new ResourceAccessDeniedHandler()));

		http.csrf(customizer -> customizer
			// 基于 RequestMatcher Bean 的配置
			.ignoringRequestMatchers(csrfIgnoreRequestMatchers)
			// 基于 URL 的配置
			.ignoringAntMatchers(securityProperties.getCsrfIgnores())
			// CSRF 配置
			.csrfTokenRepository(csrfTokenRepository));

		return http.build();
	}

	/**
	 * 此处返回值需要使用 {@link UserDetailsService} 接口的实现
	 * <p>
	 * 此处使用 {@link JdbcUserDetailsManager} 作为 {@link Bean} 的类型是因为在测试类中需要使用
	 * {@link JdbcUserDetailsManager} 的 {@link Bean}
	 * <p>
	 * @see JdbcDaoImpl#DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY 权限组查询语句
	 * @see JdbcUserDetailsManager#DEF_FIND_GROUPS_SQL 查询所有群组
	 * @see JdbcUserDetailsManager#DEF_FIND_USERS_IN_GROUP_SQL 根据群组名称查询关联的用户
	 * @see JdbcUserDetailsManager#DEF_INSERT_GROUP_SQL 创建权限组
	 * @see JdbcUserDetailsManager#DEF_FIND_GROUP_ID_SQL 根据权限组名称查询权限组ID
	 * @see JdbcUserDetailsManager#DEF_DELETE_GROUP_SQL 根据群组ID删除群组
	 * @see JdbcUserDetailsManager#DEF_RENAME_GROUP_SQL 重命名群组
	 * @see JdbcUserDetailsManager#DEF_GROUP_AUTHORITIES_QUERY_SQL 根据群组名称查询权限
	 */
	@Bean
	public JdbcUserDetailsManager userDetailsService(DataSource dataSource) {
		JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(dataSource);

		// 是否开启权限组
		boolean enableGroups = securityProperties.isEnableGroups();
		jdbcUserDetailsManager.setEnableGroups(enableGroups);

		if (enableGroups) {
			// 转译：在开启权限组时，转译关键字 groups（在 Linux MySQL 时会出现关键字问题）
			// @formatter:off
			// 使用用户名查询权限组
			jdbcUserDetailsManager.setGroupAuthoritiesByUsernameQuery(JdbcDaoImpl.DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY.replace("groups", "`groups`"));
			// 查询所有群组
			jdbcUserDetailsManager.setFindAllGroupsSql(JdbcUserDetailsManager.DEF_FIND_GROUPS_SQL.replace("groups", "`groups`"));
			// 根据群组名称查询关联的用户
			jdbcUserDetailsManager.setFindUsersInGroupSql(JdbcUserDetailsManager.DEF_FIND_USERS_IN_GROUP_SQL.replace("groups", "`groups`"));
			// 创建权限组
			jdbcUserDetailsManager.setInsertGroupSql(JdbcUserDetailsManager.DEF_INSERT_GROUP_SQL.replace("groups", "`groups`"));
			// 根据权限组名称查询权限组ID
			jdbcUserDetailsManager.setFindGroupIdSql(JdbcUserDetailsManager.DEF_FIND_GROUP_ID_SQL.replace("groups", "`groups`"));
			// 根据群组ID删除群组
			jdbcUserDetailsManager.setDeleteGroupSql(JdbcUserDetailsManager.DEF_DELETE_GROUP_SQL.replace("groups", "`groups`"));
			// 重命名群组
			jdbcUserDetailsManager.setRenameGroupSql(JdbcUserDetailsManager.DEF_RENAME_GROUP_SQL.replace("groups", "`groups`"));
			// 根据群组名称查询权限
			jdbcUserDetailsManager.setGroupAuthoritiesSql(JdbcUserDetailsManager.DEF_GROUP_AUTHORITIES_QUERY_SQL.replace("groups", "`groups`"));
			// @formatter:on
		}

		return jdbcUserDetailsManager;
	}

}
